/*
Copyright (c) 2008, Mike Gashler

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <stdarg.h>
#ifdef WIN32
#	include <direct.h>
#	include <io.h>
#	include <windows.h>
#	include <Shlobj.h>
#	include <process.h>
#else
#	include <sys/stat.h>
#	include <signal.h>
#	include <sys/wait.h>
#endif // WIN32
#include <errno.h>
#include "app.h"
#include "sha2.h"
#include <time.h>

using namespace std;

char* g_szErrorMessage = NULL;
int g_tempID = 0;

// This is a kludged substitute for vcprintf, which I can't seem to find in Linux
int MeasureFormattedStringLength(const char* szFormat, va_list args)
{
	int nSize = 0;
	const char* sz = szFormat;
	while(*sz != '\0')
	{
		if(*sz == '%')
		{
			sz++;
			switch(*sz)
			{
				case 'c':
					nSize += 1;
					va_arg(args, int);
					break;
				case 's':
					nSize += (int)strlen(va_arg(args, char*));
					break;
				case 'd':
					nSize += 10;
					va_arg(args, int);
					break;
				case 'l':
					sz++;
					if(*sz == 's')
						nSize += (int)wcslen(va_arg(args, wchar_t*));
					else
					{
						nSize += 20;
						va_arg(args, double);
					}
					break;
				case 'f':
					nSize += 20;
					va_arg(args, double/*float*/);
					break;
				case 'g':
					nSize += 20;
					va_arg(args, double/*float*/);
					break;
				default:
					nSize += 20; // take a guess
					break;
			}
		}
		sz++;
		nSize++;
	}
	return nSize;
}

void ThrowError(const char* szFormat, ...)
{
	int nSize = 0;
	{
		va_list args;
		va_start(args, szFormat);
		nSize = MeasureFormattedStringLength(szFormat, args);
		va_end(args);
	}

	// Allocate the buffer
	char* pOldMessage = g_szErrorMessage;
	g_szErrorMessage = new char[nSize + 2];

	// Format the message
	{
		va_list args;
		va_start(args, szFormat);
		{
#ifdef _MBCS
			int res = vsprintf(g_szErrorMessage, szFormat, args);
#else
			int res = vsnprintf(g_szErrorMessage, nSize + 1, szFormat, args);
#endif
			if(res < 0)
				throw("Error formatting string in ThrowError");
		}
		va_end(args);
	}
	g_szErrorMessage[nSize + 1] = '\0';
	delete[] pOldMessage;

	// Throw the error
#ifdef _DEBUG
	//fprintf(stderr, "About to throw the following exception: %s\n(This might be a good place to put a breakpoint.)\n", g_szErrorMessage);
#endif // _DEBUG
	throw (const char*)g_szErrorMessage;
}


int measureParamLen(const char* sz)
{
	int len = 0;
	while(true)
	{
		if(*sz == '"')
		{
			len++;
			sz++;
			while(*sz != '"' && *sz != '\0')
			{
				len++;
				sz++;
			}
			if(*sz == '"')
			{
				len++;
				sz++;
			}
			continue;
		}
		else if(*sz == '\'')
		{
			len++;
			sz++;
			while(*sz != '\'' && *sz != '\0')
			{
				len++;
				sz++;
			}
			if(*sz == '\'')
			{
				len++;
				sz++;
			}
			continue;
		}
		else if(*sz <= ' ')
			break;
		len++;
		sz++;
	}
	return len;
}

int measureWhitespaceLen(const char* sz)
{
	int len = 0;
	while(*sz <= ' ' && *sz != '\0')
	{
		len++;
		sz++;
	}
	return len;
}

int CountArgs(const char* sz)
{
	int count = 0;
	while(true)
	{
		sz += measureWhitespaceLen(sz);
		if(*sz == '\0')
			break;
		count++;
		sz += measureParamLen(sz);
	}
	return count;
}

void ParseArgs(char* sz, char* argv[], int cap)
{
	int count = 0;
	while(true)
	{
		if(*sz != '\0' && *sz <= ' ')
		{
			*sz = '\0';
			sz++;
		}
		sz += measureWhitespaceLen(sz);
		if(*sz == '\0')
			break;
		argv[count++] = sz;
		if(count >= cap)
			break;
		sz += measureParamLen(sz);
	}
	argv[count] = NULL;
}

int SystemCall(const char* szCommand, bool wait)
{
#ifdef WIN32
	// Parse the args
	GTEMPBUF(char, szCopy, (int)strlen(szCommand) + 1);
	strcpy(szCopy, szCommand);
	int argc = CountArgs(szCopy);
	if(argc == 0)
		return 0;
	char* argv[3];
	ParseArgs(szCopy, argv, 2);

	// Call it
	SHELLEXECUTEINFO sei;
	memset(&sei, '\0', sizeof(SHELLEXECUTEINFO));
	sei.cbSize = sizeof(SHELLEXECUTEINFO);
	sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_DDEWAIT;
	sei.hwnd = NULL;
	sei.lpVerb = NULL;
	sei.lpFile = argv[0];
	sei.lpParameters = argv[1];
	sei.lpDirectory = NULL;
	sei.nShow = SW_SHOW;
	if(!ShellExecuteEx(&sei))
		ThrowError("An error occurred while executing the command \"%s %s\"", argv[0], argv[1]);
	int ret = 0;
	if(wait)
	{
		WaitForSingleObject(sei.hProcess, INFINITE);
		DWORD dwRet;
		if(!GetExitCodeProcess(sei.hProcess, &dwRet))
		{
			CloseHandle(sei.hProcess);
			ThrowError("Failed to obtain exit code");
		}
		ret = dwRet;
		CloseHandle(sei.hProcess);
	}
	return (int)ret;
#else
	GTEMPBUF(char, buf, strlen(szCommand) + 4);
	strcpy(buf, szCommand);
	if(!wait)
		strcat(buf, " &");
	int status = system(buf);
	if(status == -1)
		ThrowError("Failed to execute command");
	return WEXITSTATUS(status);
#endif
}

int SystemExecute(const char* szCommand, bool wait, const char* szStdOutFilename, const char* szStdErrFilename)
{
#ifdef WIN32
	// Set the bInheritHandle flag so pipe handles are inherited.
	SECURITY_ATTRIBUTES saAttr;
	saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
	saAttr.bInheritHandle = TRUE; // pipe handles are inherited
	saAttr.lpSecurityDescriptor = NULL;

	// Create pipes for the child process's stdout and stdin
	HANDLE hChildStdinRd, hChildStdinWr, hChildStdoutRd, hChildStdoutWr, hChildStderrRd, hChildStderrWr;
	if(!CreatePipe(&hChildStderrRd, &hChildStderrWr, &saAttr, 0))
		ThrowError("Failed to create a pipe for the child processes stderr");
	SetHandleInformation(hChildStderrRd, HANDLE_FLAG_INHERIT, 0); // Ensure that the read handle to the child process's pipe for stdout is not inherited
	if(!CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0))
		ThrowError("Failed to create a pipe for the child processes stdout");
	SetHandleInformation(hChildStdoutRd, HANDLE_FLAG_INHERIT, 0); // Ensure that the read handle to the child process's pipe for stdout is not inherited
	if(!CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0))
		ThrowError("Failed to create a pipe for child processes stdin");
	SetHandleInformation( hChildStdinWr, HANDLE_FLAG_INHERIT, 0); // Ensure that the write handle to the child process's pipe for stdin is not inherited.

	// Create the child process.
	STARTUPINFO siStartInfo;
	ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
	siStartInfo.cb = sizeof(STARTUPINFO); 
	siStartInfo.hStdError = hChildStderrWr;
	siStartInfo.hStdOutput = hChildStdoutWr;
	siStartInfo.hStdInput = hChildStdinRd;
	siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
	PROCESS_INFORMATION piProcInfo;
	ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
//printf("Path=%s\n", getenv("PATH"));
	if(!CreateProcess(NULL,
		(LPSTR)szCommand,  // command line
		NULL,          // process security attributes
		NULL,          // primary thread security attributes
		TRUE,          // handles are inherited
		0, /*CREATE_NO_WINDOW*/             // creation flags
		NULL,          // NULL means use parent's environment
		NULL,          // NULL means use parent's current directory
		&siStartInfo,  // STARTUPINFO pointer
		&piProcInfo))  // receives PROCESS_INFORMATION
		ThrowError("Failed to execute the command: \"%s\". Please make sure this application is installed and that its location is included in your PATH environment variable.", szCommand);

	int ret = 0;
	if(wait)
	{
		WaitForSingleObject(piProcInfo.hProcess, INFINITE);
		DWORD dwRet;
		if(!GetExitCodeProcess(piProcInfo.hProcess, &dwRet))
		{
			CloseHandle(piProcInfo.hProcess);
			CloseHandle(piProcInfo.hThread);
			ThrowError("Failed to obtain exit code");
		}
		ret = dwRet;
	}
	CloseHandle(piProcInfo.hProcess);
	CloseHandle(piProcInfo.hThread);

	// Close the child processes' stdin pipe, since we don't really need it
	CloseHandle(hChildStdoutWr);
	CloseHandle(hChildStderrWr);

	// Read the output from the child process and save to file
	if(szStdErrFilename)
	{
		DWORD dwRead;
		CHAR chBuf[4096];
		FILE* pFileOut = fopen(szStdErrFilename, "wb");
		FileHolder hFileOut(pFileOut);
		while(true)
		{
			if(!ReadFile(hChildStderrRd, chBuf, 4096, &dwRead, NULL) || dwRead == 0)
				break;
			if(fwrite(chBuf, dwRead, 1, pFileOut) != 1)
				ThrowError("Failed to write to the stderr file");
		}
	}
	if(szStdOutFilename)
	{
		DWORD dwRead;
		CHAR chBuf[4096];
		FILE* pFileOut = fopen(szStdOutFilename, "wb");
		FileHolder hFileOut(pFileOut);
		while(true)
		{
			if(!ReadFile(hChildStdoutRd, chBuf, 4096, &dwRead, NULL) || dwRead == 0)
				break;
			if(fwrite(chBuf, dwRead, 1, pFileOut) != 1)
				ThrowError("Failed to write to the stdout file");
		}
	}

	// Clean up
	CloseHandle(hChildStdinWr);
	CloseHandle(hChildStdinRd);
	CloseHandle(hChildStdoutRd);
	CloseHandle(hChildStderrRd);

	return ret;
#else
	// Parse the args
	GTEMPBUF(char, szCopy, strlen(szCommand) + 1);
	strcpy(szCopy, szCommand);
	int argc = CountArgs(szCopy);
	if(argc == 0)
		return 0;
	char* argv[argc + 1];
	ParseArgs(szCopy, argv, 2147483647);

	// Call it
	pid_t pid = fork();
	if(pid == 0)
	{
		// Redirect the output streams
		if(szStdOutFilename)
			freopen(szStdOutFilename, "wb", stdout);
		else
			freopen("/dev/null", "wb", stdout);
		if(szStdErrFilename)
			freopen(szStdErrFilename, "wb", stderr);
		else
			freopen("/dev/null", "wb", stderr);

		// Call the child process
		execvp(argv[0], argv);

		// execvp only returns if there is an error. Otherwise, it replaces the current process.
		cout << "Error calling execvp. errno=" << errno << "\n";
		ThrowError("Error calling execvp. errno=%d", errno);
	}
	else if(pid > 0)
	{
		// The calling process
		if(wait)
		{
			int status;
			waitpid(pid, &status, 0);
			if(WIFEXITED(status))
			{
				int ret = WEXITSTATUS(status);
				return ret;
			}
			else if(WIFSIGNALED(status))
				ThrowError("The process was interruped with signal %d", WSTOPSIG(status));
			else
				ThrowError("The process stopped without exiting, and it cannot be restarted.");
		}
	}
	else
		ThrowError("There was an error forking the process");
	return 0;
#endif
}

void GetTempFilename(char* pBuf)
{
#ifdef WIN32
	char tmp[512];
	GetTempPath(512, tmp);
	tmp[256] = '\0';
	int len = strlen(tmp);
	if(len > 0 && tmp[len - 1] != '\\' && tmp[len - 1] != '/')
		strcpy(tmp + len, "/");
	time_t t;
	sprintf(pBuf, "%sgpr%d%d%d.tmp", tmp, (int)time(&t), _getpid(), g_tempID++);
#else
	time_t t;
	sprintf(pBuf, "/tmp/gpr%d%d%d.tmp", (int)time(&t), getpid(), g_tempID++);
#endif
}

char* LoadFileToBuffer(const char* filename, size_t* pOutSize)
{
	*pOutSize = 0;
	FILE* pFile = fopen(filename, "rb");
	if(!pFile)
	{
		if(access(filename, 0) == 0)
			ThrowError("Error opening the file %s for reading. Do you have permissions to read this file?", filename);
		else
			ThrowError("The file \"%s\" does not exist", filename);
	}
	FileHolder hFile(pFile);
	size_t fileSize = filelength(fileno(pFile));
	*pOutSize = fileSize;
	char* pBuf = new char[fileSize + 1];
	ArrayHolder<char*> hBuf(pBuf);
	if(fileSize > 0)
	{
		if(fread(pBuf, fileSize, 1, pFile) != 1)
			ThrowError("Error reading file %s", filename);
		if(ferror(pFile) != 0)
			ThrowError("Error reading file %s", filename);
	}
	pBuf[fileSize] = '\0';
	return hBuf.Drop();
}

void ParseFilename(const char* szPath, struct PathData* pData)
{
#ifdef WIN32
	if(szPath[0] != '\0' && szPath[1] == ':')
		pData->dirStart = 2;
	else
		pData->dirStart = 0;
#else
	pData->dirStart = 0;
#endif // !WIN32
	int n;
	int lastSlash = -1;
	int lastPeriod = -1;
	for(n = pData->dirStart; szPath[n] != '\0'; n++)
	{
		if(szPath[n] == '/'
#ifdef WIN32
			|| szPath[n] == '\\'
#endif // WIN32
			)
			lastSlash = n;
		else if(szPath[n] == '.')
			lastPeriod = n;
	}
	pData->fileStart = lastSlash + 1;
	if(lastPeriod <= lastSlash)
		pData->extStart = n;
	else
		pData->extStart = lastPeriod;
	pData->len = n;
}

char* NormalizeTextFile(const char* before, size_t sizeBefore, size_t* pSizeAfter)
{
	char* after = new char[sizeBefore + 2];
	size_t in = 0;
	size_t out = 0;
	while(in < sizeBefore)
	{
		if(before[in] == '\r')
		{
			if(in + 1 >= sizeBefore || before[in + 1] != '\n')
				after[out++] = '\n';
		}
		else if(before[in] == '\0')
			break;
		else
			after[out++] = before[in];
		in++;
	}
	if(out == 0 || after[out - 1] != '\n')
		after[out++] = '\n';
	*pSizeAfter = out;
	after[out] = '\0';
	return after;
}

int strlinelen(const char* str)
{
	int len = 0;
	while(*str != '\0' && *str != '\n' && *str != '\r')
	{
		str++;
		len++;
	}
	return len;
}

#ifndef WIN32
void DeleteFile(const char* filename)
{
	GTEMPBUF(char, buf, strlen(filename) + 12);
	sprintf(buf, "rm %s", filename);
	SystemExecute(buf, true, NULL, NULL);
}

long filelength(int filedes)
{
	struct stat s;
	if(fstat(filedes, &s) == -1)
		return 0;
	return s.st_size;
}

int stricmp(const char* szA, const char* szB)
{
	while(*szA)
	{
		if((*szA | 32) < (*szB | 32))
			return -1;
		if((*szA | 32) > (*szB | 32))
			return 1;
		szA++;
		szB++;
	}
	if(*szB)
		return -1;
	return 0;
}

int strnicmp(const char* szA, const char* szB, int len)
{
	int n;
	for(n = 0; n < len; n++)
	{
		if((*szA | 32) < (*szB | 32))
			return -1;
		if((*szA | 32) > (*szB | 32))
			return 1;
		szA++;
		szB++;
	}
	return 0;
}

char* itoa(int n, char* szBuf, int b)
{
	sprintf(szBuf, "%d", n);
	return szBuf;
}
#endif

void ByteToHexBigEndian(unsigned char byte, char* pHex)
{
	pHex[1] = (byte & 15) + '0';
	if(pHex[1] > '9')
		pHex[1] += ('a' - '0' - 10);
	pHex[0] = (byte >> 4) + '0';
	if(pHex[0] > '9')
		pHex[0] += ('a' - '0' - 10);
}

unsigned char HexToByte(char msb, char lsb)
{
	char v1, v2;
	if(lsb <= '9')
		v1 = lsb - '0';
	else if(lsb <= 'Z')
		v1 = lsb - 'A' + 10;
	else
		v1 = lsb - 'a' + 10;
	if(msb <= '9')
		v2 = msb - '0';
	else if(msb <= 'Z')
		v2 = msb - 'A' + 10;
	else
		v2 = msb - 'a' + 10;
	return(v1 | (v2 << 4));
}

void BufferToHexBigEndian(FILE* pOutStream, const unsigned char* pBuffer, int nBufferSize)
{
	char hex[3];
	hex[2] = '\0';
	int n;
	for(n = 0; n < nBufferSize; n++)
	{
		ByteToHexBigEndian(pBuffer[n], hex);
		fprintf(pOutStream, hex);
	}
}

void GetAppFolder(char* buf, int bufLen, const char* appName)
{
#ifdef WIN32
	char home[300];
	if(!SHGetSpecialFolderPath(NULL, home, CSIDL_LOCAL_APPDATA, TRUE))
			ThrowError("Failed to obtain local-app-data folder");
	int homeLen = strlen(home);
	if(homeLen < 1)
		ThrowError("Expected the \"HOME\" env var to be set");
	int appNameLen = strlen(appName);
	if(bufLen < homeLen + appNameLen + 4)
		ThrowError("Buffer not big enough to hold home folder");
	strcpy(buf, home);
	if(home[homeLen - 1] != '/')
	{
		buf[homeLen] = '/';
		homeLen++;
	}
	strcpy(buf + homeLen, appName);
	strcpy(buf + homeLen + appNameLen, "/");
	mkdir(buf);
#else
	const char* home = getenv("HOME");
	if(!home)
		ThrowError("Expected the \"HOME\" env var to be set");
	int homeLen = strlen(home);
	if(homeLen < 1)
		ThrowError("Expected the \"HOME\" env var to be set");
	int appNameLen = strlen(appName);
	if(bufLen < homeLen + appNameLen + 4)
		ThrowError("Buffer not big enough to hold home folder");
	strcpy(buf, home);
	if(home[homeLen - 1] != '/')
	{
		buf[homeLen] = '/';
		homeLen++;
	}
	buf[homeLen] = '.';
	strcpy(buf + homeLen + 1, appName);
	strcpy(buf + homeLen + 1 + appNameLen, "/");
	mkdir(buf, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); // read/write/search permissions for owner and group, and with read/search permissions for others
#endif
}

void Sha512DigestFile(unsigned char* pOut64ByteHash, const char* filename)
{
	// Digest the file
	unsigned char* buf = new unsigned char[8192];
	ArrayHolder<unsigned char*> hBuf(buf);
	sha512_ctx ctx;
	sha512_begin(&ctx);
	FILE* pFileIn = fopen(filename, "rb");
	if(!pFileIn)
		ThrowError("could not open the file \"%s\"", filename);
	FileHolder hFile(pFileIn);
	int fileSize = filelength(fileno(pFileIn));
	int size;
	while(fileSize > 0)
	{
		size = MIN(fileSize, 8192);
		if(fread(buf, size, 1, pFileIn) != 1)
			ThrowError("error reading from file \"%s\"", filename);
		if(ferror(pFileIn) != 0)
			ThrowError("error reading file \"%s\"", filename);
		sha512_hash(buf, size, &ctx);
		fileSize -= size;
	}
	sha512_end(pOut64ByteHash, &ctx);
}

void CondensePath(char* szPath)
{
	int n;
#ifdef WIN32
	for(n = 0; szPath[n] != '\0'; n++)
	{
		if(szPath[n] == '\\')
			szPath[n] = '/';
	}
#endif // WIN32
	int nPrevSlash = -1;
	int nPrevPrevSlash = -1;
	for(n = 0; szPath[n] != '\0'; n++)
	{
		if(szPath[n] == '/')
		{
			nPrevPrevSlash = nPrevSlash;
			nPrevSlash = n;
			if(strncmp(szPath + n, "/./", 3) == 0)
			{
				int nDelSize = 2;
				int i;
				for(i = n + 1; ; i++)
				{
					szPath[i] = szPath[i + nDelSize];
					if(szPath[i] == '\0')
						break;
				}
				n--; // so we'll catch the current slash the next time around the loop -- handle it next time!
				
			}
			else if(nPrevPrevSlash >= 0 && strncmp(szPath + n, "/../", 4) == 0)
			{
				int nDelSize = n - nPrevPrevSlash + 3;
				int i;
				for(i = nPrevPrevSlash; ; i++)
				{
					szPath[i] = szPath[i + nDelSize];
					if(szPath[i] == '\0')
						break;
				}
				nPrevSlash = -1;
				nPrevPrevSlash = -1;
				n = -1;
			}
				
		}
	}
}

int SafeStrCpy(char* szDest, const char* szSrc, int nMaxSize)
{
	nMaxSize--;
	if(nMaxSize < 0)
		return 0;
	int n;
	for(n = 0; szSrc[n] != '\0' && n < nMaxSize; n++)
		szDest[n] = szSrc[n];
	szDest[n] = '\0';
	return n;
}
